#include "MiniOpenGLWidget.h"
#include <QCoreApplication>

#define MIN(a, b) ((a) > (b) ? (b) : (a))
#define MAX(a, b) ((a) < (b) ? (b) : (a))

MiniOpenGLWidget::MiniOpenGLWidget(QWidget *parent) :
    QOpenGLWidget(parent), m_mode(GL_FILL), m_status(normal), m_limit(false), m_screen(0, 0), m_catching(nullptr)
{
    // 开启鼠标追踪
    setMouseTracking(true);

    // 初始化光照参数
    m_lighting.color = {1.0, 1.0, 1.0};
    m_lighting.pos = {0.0, 12.0, 0.0};
    m_lighting.decrease = {0.005, 0.001, 0.0};

    // 初始化材质信息
    float R[] = {0.3, 0.3, 0.3, 0.4, 0.4, 0.4, 0.5, 0.5, 0.5};
    m_material.alpha = 100;
    m_material.R = QMatrix3x3(R);
}

MiniOpenGLWidget::~MiniOpenGLWidget()
{
    // 判断 OpenGL 资源是否成功初始化
    if (!isValid())
        return;

    makeCurrent();

    // 销毁对象
    glDeleteBuffers(1, &m_VBO);
    glDeleteVertexArrays(1, &m_VAO);

    doneCurrent();

    for (auto handle : m_handlers)
        delete handle;
}

void MiniOpenGLWidget::setWireframe(bool wireframe)
{
    m_mode = wireframe ? GL_LINE : GL_FILL;
    update();
}

void MiniOpenGLWidget::setLighting(lighting_data data)
{
    m_lighting = data;
    update();
}

void MiniOpenGLWidget::setMaterial(material_data data)
{
    m_material = data;
    update();
}

void MiniOpenGLWidget::setControlPoints(bool limit)
{
    m_limit = limit;
    update();
}

MiniHandler *MiniOpenGLWidget::registerObject(MiniObject *obj)
{
    int type = obj->GetType();
    switch (type)
    {
    case MiniObject::cloud:
    {
        m_handlers.push_back(new MiniCloudHandler(obj));
        m_handlers.back()->SetBasisColor({0.0, 1.0, 1.0});
        break;
    }
    case MiniObject::mesh:
    {
        m_handlers.push_back(new MiniMeshHandler(obj));
        break;
    }
    case MiniObject::discrete_curve:
    {
        m_handlers.push_back(new MiniDiscreteCurveHandler(obj));
        break;
    }
    case MiniObject::B_spline_curve:
    {
        m_handlers.push_back(new MiniBSplineCurveHandler(obj));
        break;
    }
    case MiniObject::B_spline_surface:
    {
        m_handlers.push_back(new MiniBSplineSurfaceHandler(obj));
        m_handlers.back()->SetBasisColor({1.0, 1.0, 0});
        break;
    }
    case MiniObject::para_curve:
    {
        m_handlers.push_back(new MiniParaCurveHandler(obj));
        break;
    }
    case MiniObject::para_surface:
    {
        m_handlers.push_back(new MiniParaSurfaceHandler(obj));
        m_handlers.back()->SetBasisColor({1.0, 1.0, 0});
        break;
    }
    }
    return m_handlers.back();
}

void MiniOpenGLWidget::drawObject(MiniHandler *handler)
{
    // 如果限制显示，就只显示一个
    int n = m_limit ? 1 : handler->GetObjNum();

    // 绘制 handler 中的图元
    for (int i = 0; i < n; i++)
    {
        // 绑定顶点数组，所有缓冲属性都要围绕顶点数组展开
        glBindVertexArray(m_VAO);

        // 绑定 VBO 数据
        glBindBuffer(GL_ARRAY_BUFFER, m_VBO);
        glBufferData(GL_ARRAY_BUFFER,
                     sizeof (float) * handler->GetVerticesCount(i) * 3,
                     handler->GetVertices(i),
                     GL_STATIC_DRAW);

        // 传入光照信息
        m_shader.setUniformValue("ViewerPos", m_camera.GetPosition());
        m_shader.setUniformValue("Material.alpha", m_material.alpha);
        m_shader.setUniformValue("Material.R", m_material.R);
        m_shader.setUniformValue("Lighting.pos", m_lighting.pos);
        m_shader.setUniformValue("Lighting.decrease", m_lighting.decrease);

        // 获得绘图模式并传入
        GLenum mode = handler->GetMode(i);
        if (handler->GetMode(i) == GL_TRIANGLES)
        {
            m_shader.setUniformValue("IfLighting", true);
            m_shader.setUniformValue("Lighting.color", handler->GetColor(i));

            // 网格/曲面的顶点数组包含顶点位置和法向的信息，用于渲染光照
            glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof (float), (void*)0);
            glEnableVertexAttribArray(0);

            // 顶点和法向间隔排列
            glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof (float), (void*)(3 * sizeof (float)));
            glEnableVertexAttribArray(1);
        }
        else if (handler->GetMode(i) == GL_LINE_STRIP)
        {
            glLineWidth(handler->GetLineWidth(i));
            m_shader.setUniformValue("IfLighting", false);
            m_shader.setUniformValue("Lighting.color", handler->GetColor(i));

            glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof (float), (void*)0);
            glEnableVertexAttribArray(0);
        }
        else
        {
            glPointSize(5);
            m_shader.setUniformValue("IfLighting", false);
            m_shader.setUniformValue("Lighting.color", handler->GetColor(i));

            glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof (float), (void*)0);
            glEnableVertexAttribArray(0);
        }

        // 绘制图元
        glDrawElements(mode, handler->GetIndicesCount(i), GL_UNSIGNED_INT, handler->GetIndices(i));
    }
}

void MiniOpenGLWidget::checkObject(QPointF pos)
{
    // 将屏幕坐标转换为空间坐标
    QPointF space(2.0 * pos.x() / width() - 1, 1 - 2.0 * pos.y() / height());

    QMatrix4x4 projection;
    projection.perspective(45, 1.0 * width() / height(), 0.1, 1000);
    projection = projection * m_camera.GetViewMatrix();

    // 注意这里 CheckCollision 要放在前面，否则如果 check 为 true，后半句不会执行！！！
    // 筛选出最前面的图元拾取
    float z = 1;
    m_catching = nullptr;
    for (auto handler : m_handlers)
    {
        // 恢复初始颜色
        handler->SetColor(handler->GetBasisColor());
        float oz = handler->CheckCollision(space, projection);
        if (oz < z)
        {
            m_catching = handler;
            z = oz;
        }
    }

    // 拾取的图元设为绿色
    if (m_catching != nullptr)
        m_catching->SetColor({0.0, 1.0, 0.0});

    update();
}

void MiniOpenGLWidget::modifyObject(QPointF pos)
{
    // 将屏幕坐标转换为空间坐标
    QPointF space0(2.0 * m_screen.x() / width() - 1, 1 - 2.0 * m_screen.y() / height());
    QPointF space1(2.0 * pos.x() / width() - 1, 1 - 2.0 * pos.y() / height());
    QPointF d = space1 - space0;

    QMatrix4x4 projection;
    projection.perspective(45, 1.0 * width() / height(), 0.1, 1000);
    projection = projection * m_camera.GetViewMatrix();

    // 给出三个方向
    QVector4D x(1, 0, 0, 1);
    QVector4D y(0, 1, 0, 1);
    QVector4D z(0, 0, 1, 1);

    // 计算它们的投影
    x = projection * x;
    y = projection * y;
    z = projection * z;

    // 规范化
    x = x / x.w();
    y = y / y.w();
    z = z / z.w();

    // 计算在三个方向上的投影长度
    float lx = sqrt(x.x() * x.x() + x.y() * x.y());
    float ly = sqrt(y.x() * y.x() + y.y() * y.y());
    float lz = sqrt(z.x() * z.x() + z.y() * z.y());

    // 防止除以零
    float px = lx < 1e-16 ? 0 : (d.x() * x.x() + d.y() * x.y()) / lx;
    float py = ly < 1e-16 ? 0 : (d.y() * y.x() + d.y() * y.y()) / ly;
    float pz = lz < 1e-16 ? 0 : (d.x() * z.x() + d.y() * z.y()) / lz;

    // 距离越远，变化越快
    Vector3f dv;
    float p = MAX(fabs(px), MAX(fabs(py), fabs(pz)));
    if (p == fabs(px))
        dv.rx() = 1.0 * px * m_camera.GetDistance();
    if (p == fabs(py))
        dv.ry() = 1.0 * py * m_camera.GetDistance();
    if (p == fabs(pz))
        dv.rz() = 1.0 * pz * m_camera.GetDistance();

    // 应用顶点变更
    m_catching->ChangeVertex(dv);
    update();
}

void MiniOpenGLWidget::initializeGL()
{
    // 初始化 OpenGL 函数
    initializeOpenGLFunctions();

    // 创建 VBO, VAO, EBO 对象，并赋予 ID
    glGenVertexArrays(1, &m_VAO);
    glGenBuffers(1, &m_VBO);
    // 不使用 EBO，而是直接通过外部索引数组

    // 链接着色器
    bool success;
    m_shader.addShaderFromSourceFile(QOpenGLShader::Vertex, ":/Shader/shader.vert");
    m_shader.addShaderFromSourceFile(QOpenGLShader::Fragment, ":/Shader/shader.frag");
    success = m_shader.link();
    if (!success)
        qDebug() << "ERR:" << m_shader.log();
}

void MiniOpenGLWidget::paintGL()
{
    // 初始化并计算变换矩阵
    QMatrix4x4 projection;
    projection.perspective(45, 1.0 * width() / height(), 0.1, 100);

    // 将变换矩阵传入顶点着色器
    m_shader.setUniformValue("view", m_camera.GetViewMatrix());
    m_shader.setUniformValue("projection", projection);

    // 绘图模式
    glPolygonMode(GL_FRONT_AND_BACK, m_mode);

    // 重新填充背景
    glEnable(GL_DEPTH_TEST);
    glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // 启用反走样
    glEnable(GL_POINT_SMOOTH);
    glEnable(GL_LINE_SMOOTH);

    // 绑定渲染程序
    m_shader.bind();

    // 绘制物体
    for (auto handler : m_handlers)
        drawObject(handler);
}

void MiniOpenGLWidget::wheelEvent(QWheelEvent *event)
{   
    QPoint delta = event->angleDelta();
    float d = m_camera.GetDistance();
    if (delta.y() > 0)
        d -= 1.0;
    else
        d += 1.0;
    if (d <= 0)
        d = 0;
    m_camera.SetDistance(d);

    // 检查碰撞
    checkObject(event->pos());
}

void MiniOpenGLWidget::mousePressEvent(QMouseEvent *event)
{
    // 如果鼠标按下时捕获到物体，就进入移动物体的状态
    if (m_catching != nullptr)
        m_status = catching;
    else
    {
        m_status = normal;

        // 设置相机移动
        m_screen = event->pos();
        m_camera.SetMoving(true);
    }
}

void MiniOpenGLWidget::mouseReleaseEvent(QMouseEvent *)
{
    m_camera.SetMoving(false);
    m_status = normal;
}

void MiniOpenGLWidget::mouseMoveEvent(QMouseEvent *event)
{
    QPoint pos = event->pos();

    // 当相机处于可移动状态时进行处理
    if (m_camera.GetMoving())
    {
        int dx = pos.x() - m_screen.x();
        int dy = pos.y() - m_screen.y();

        float pitch = m_camera.GetPitch();
        float yaw = m_camera.GetYaw();
        float rate = m_camera.GetRate();

        // 注意限制纵向的角度
        m_camera.SetYaw(yaw - dx * rate);
        m_camera.SetPitch(MIN(MAX(pitch + dy * rate, -1.5), 1.5));
    }

    // 检查碰撞，修改捕获的物体指针
    if (m_status == normal)
        checkObject(pos);
    // 处于捕获状态，不会修改捕获的指针
    else
        modifyObject(pos);

    // 总是更新屏幕坐标
    m_screen = pos;
}
